#pragma once
#include <array>
#include <cmath>
#include <initializer_list>

#include "../PocelainPrerequisites.h"

POCELAIN_NAMESPACE_BEGIN
template <class T, size_t d> static void TFuncArraySet(std::array<T, d> &arr, T val)
{
    for (size_t i = 0; i < d; ++i)
    {
        arr[i] = val;
    }
}
template <class T> static void TFuncArraySet(T arr[], size_t size, T val)
{
    for (size_t i = 0; i < size; ++i)
    {
        arr[i] = val;
    }
}
template <class T, size_t d> static void TFuncArrayNegate(std::array<T, d> &arr)
{
    for (size_t i = 0; i < d; ++i)
    {
        arr[i] = -arr[i];
    }
}
template <class T> static void TFuncArrayNegate(T arr[], size_t size)
{
    for (size_t i = 0; i < size; ++i)
    {
        arr[i] = -arr[i];
    }
}
template <class T, size_t d> static void TFuncArrayAdd(std::array<T, d> &arr1, const std::array<T, d> &arr2)
{
    for (size_t i = 0; i < d; ++i)
    {
        arr1[i] += arr2[i];
    }
}
template <class T> static void TFuncArrayAdd(T arr1[], const T arr2[], size_t size)
{
    for (size_t i = 0; i < size; ++i)
    {
        arr1[i] += arr2[i];
    }
}
template <class T, size_t d> static void TFuncArraySub(std::array<T, d> &arr1, const std::array<T, d> &arr2)
{
    for (size_t i = 0; i < d; ++i)
    {
        arr1[i] -= arr2[i];
    }
}
template <class T> static void TFuncArraySub(T arr1[], const T arr2[], size_t size)
{
    for (size_t i = 0; i < size; ++i)
    {
        arr1[i] -= arr2[i];
    }
}
template <class T, size_t d> static void TFuncArrayMultiply(std::array<T, d> &arr1, const std::array<T, d> &arr2)
{
    for (size_t i = 0; i < d; ++i)
    {
        arr1[i] *= arr2[i];
    }
}
template <class T> static void TFuncArrayMultiply(T arr1[], const T arr2[], size_t size)
{
    for (size_t i = 0; i < size; ++i)
    {
        arr1[i] *= arr2[i];
    }
}
template <class T, size_t d> static void TFuncArrayDivide(std::array<T, d> &arr1, const std::array<T, d> &arr2)
{
    for (size_t i = 0; i < d; ++i)
    {
        arr1[i] /= arr2[i];
    }
}
template <class T> static void TFuncArrayDivide(T arr1[], const T arr2[], size_t size)
{
    for (size_t i = 0; i < size; ++i)
    {
        arr1[i] /= arr2[i];
    }
}
template <class T, size_t d> static void TFuncArrayAddScalar(std::array<T, d> &arr1, T scalar)
{
    for (size_t i = 0; i < d; ++i)
    {
        arr1[i] += scalar;
    }
}
template <class T> static void TFuncArrayAddScalar(T arr1[], const T scalar, size_t size)
{
    for (size_t i = 0; i < size; ++i)
    {
        arr1[i] += scalar;
    }
}
template <class T, size_t d> static void TFuncArraySubScalar(std::array<T, d> &arr1, T scalar)
{
    for (size_t i = 0; i < d; ++i)
    {
        arr1[i] -= scalar;
    }
}
template <class T> static void TFuncArraySubScalar(T arr1[], const T scalar, size_t size)
{
    for (size_t i = 0; i < size; ++i)
    {
        arr1[i] -= scalar;
    }
}
template <class T, size_t d> static void TFuncArrayMultiplyScalar(std::array<T, d> &arr1, T scalar)
{
    for (size_t i = 0; i < d; ++i)
    {
        arr1[i] *= scalar;
    }
}
template <class T> static void TFuncArrayMultiplyScalar(T arr1[], T scalar, size_t size)
{
    for (size_t i = 0; i < size; ++i)
    {
        arr1[i] *= scalar;
    }
}
template <class T, size_t d> static void TFuncArrayDivideScalar(std::array<T, d> &arr1, T scalar)
{
    for (size_t i = 0; i < d; ++i)
    {
        arr1[i] /= scalar;
    }
}
template <class T> static void TFuncArrayDivideScalar(T arr1[], T scalar, size_t size)
{
    for (size_t i = 0; i < size; ++i)
    {
        arr1[i] /= scalar;
    }
}

// TVector CommonPart
#define POCELAIN_DECLARE_VECTOR_COMMON_PART(dimension)                                                                 \
    typedef T value_type;                                                                                              \
    typedef TVector<T, dimension> type;                                                                                \
    std::array<T, dimension> elements;                                                                                 \
    TVector(std::initializer_list<T> l)                                                                                \
    {                                                                                                                  \
        this->set(l);                                                                                                  \
    }                                                                                                                  \
    TVector(const type &other)                                                                                         \
    {                                                                                                                  \
        this->copy(other);                                                                                             \
    }                                                                                                                  \
    static type zero()                                                                                                 \
    {                                                                                                                  \
        return type().setAll(T(0));                                                                                    \
    }                                                                                                                  \
    static type one()                                                                                                  \
    {                                                                                                                  \
        return type().setAll(T(1));                                                                                    \
    }                                                                                                                  \
    const T &operator[](size_t idx) const                                                                              \
    {                                                                                                                  \
        POCELAIN_ASSERT(idx < dimension);                                                                              \
        return elements[idx];                                                                                          \
    }                                                                                                                  \
    T &operator[](size_t idx)                                                                                          \
    {                                                                                                                  \
        POCELAIN_ASSERT(idx < dimension);                                                                              \
        return elements[idx];                                                                                          \
    }                                                                                                                  \
    type operator+(T s) const                                                                                          \
    {                                                                                                                  \
        type ret;                                                                                                      \
        TFuncArrayAddScalar(ret.elements, s);                                                                          \
        return ret;                                                                                                    \
    }                                                                                                                  \
    type operator-(T s) const                                                                                          \
    {                                                                                                                  \
        type ret;                                                                                                      \
        TFuncArraySubScalar(ret.elements, s);                                                                          \
        return ret;                                                                                                    \
    }                                                                                                                  \
    type operator*(T s) const                                                                                          \
    {                                                                                                                  \
        type ret;                                                                                                      \
        TFuncArrayMultiplyScalar(ret.elements, s);                                                                     \
        return ret;                                                                                                    \
    }                                                                                                                  \
    type operator/(T s) const                                                                                          \
    {                                                                                                                  \
        type ret;                                                                                                      \
        TFuncArrayDivideScalar(ret.elements, s);                                                                       \
        return ret;                                                                                                    \
    }                                                                                                                  \
    type operator+(const type &other) const                                                                            \
    {                                                                                                                  \
        type ret = *this;                                                                                              \
        TFuncArrayAdd(ret.elements, other.elements);                                                                   \
        return ret;                                                                                                    \
    }                                                                                                                  \
    type &operator+=(const type &other)                                                                                \
    {                                                                                                                  \
        TFuncArrayAdd(this->elements, other.elements);                                                                 \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type operator-(const type &other) const                                                                            \
    {                                                                                                                  \
        type ret = *this;                                                                                              \
        TFuncArraySub(ret.elements, other.elements);                                                                   \
        return ret;                                                                                                    \
    }                                                                                                                  \
    type &operator-=(const type &other)                                                                                \
    {                                                                                                                  \
        TFuncArraySub(this->elements, other.elements);                                                                 \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type operator*(const type &other) const                                                                            \
    {                                                                                                                  \
        type ret = *this;                                                                                              \
        TFuncArrayMultiply(ret.elements, other.elements);                                                              \
        return ret;                                                                                                    \
    }                                                                                                                  \
    type &operator*=(const type &other)                                                                                \
    {                                                                                                                  \
        TFuncArrayMultiply(this->elements, other.elements);                                                            \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type operator/(const type &other) const                                                                            \
    {                                                                                                                  \
        type ret = *this;                                                                                              \
        TFuncArrayDivide(ret.elements, other.elements);                                                                \
        return ret;                                                                                                    \
    }                                                                                                                  \
    type &operator/=(const type &other)                                                                                \
    {                                                                                                                  \
        TFuncArrayDivide(this->elements, other.elements);                                                              \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type &operator=(const type &other)                                                                                 \
    {                                                                                                                  \
        return this->copy(other);                                                                                      \
    }                                                                                                                  \
    type &operator=(std::initializer_list<T> l)                                                                        \
    {                                                                                                                  \
        this->set(l);                                                                                                  \
        return *this;                                                                                                  \
    }                                                                                                                  \
    bool operator==(const type &other) const                                                                           \
    {                                                                                                                  \
        for (size_t i = 0; i < dimension; ++i)                                                                         \
        {                                                                                                              \
            if (this->elements[i] != other.elements[i])                                                                \
                return false;                                                                                          \
        }                                                                                                              \
        return true;                                                                                                   \
    }                                                                                                                  \
    bool operator!=(const type &other) const                                                                           \
    {                                                                                                                  \
        return !(*this == other);                                                                                      \
    }                                                                                                                  \
    size_t size() const                                                                                                \
    {                                                                                                                  \
        return dimension;                                                                                              \
    }                                                                                                                  \
    type &copy(const type &other)                                                                                      \
    {                                                                                                                  \
        for (size_t i = 0; i < dimension; ++i)                                                                         \
        {                                                                                                              \
            this->elements[i] = other.elements[i];                                                                     \
        }                                                                                                              \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type &setAll(T value)                                                                                              \
    {                                                                                                                  \
        elements.fill(value);                                                                                          \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type &add(const type &other)                                                                                       \
    {                                                                                                                  \
        TFuncArrayAdd(this->elements, other.elements);                                                                 \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type &addScalar(T s)                                                                                               \
    {                                                                                                                  \
        TFuncArrayAddScalar(this->elements, s);                                                                        \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type &sub(const type &other)                                                                                       \
    {                                                                                                                  \
        TFuncArraySub(this->elements, other.elements);                                                                 \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type &subScalar(T s)                                                                                               \
    {                                                                                                                  \
        TFuncArraySubScalar(this->elements, s);                                                                        \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type &subVectors(const type &v1, const type &v2)                                                                   \
    {                                                                                                                  \
        *this = v1 - v2;                                                                                               \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type &multiply(const type &other)                                                                                  \
    {                                                                                                                  \
        TFuncArrayMultiply(this->elements, other.elements);                                                            \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type &multiplyScalar(T s)                                                                                          \
    {                                                                                                                  \
        TFuncArrayMultiplyScalar(this->elements, s);                                                                   \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type &divide(const type &other)                                                                                    \
    {                                                                                                                  \
        TFuncArrayDivide(this->elements, other.elements);                                                              \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type &divideScalar(T s)                                                                                            \
    {                                                                                                                  \
        TFuncArrayDivideScalar(this->elements, s);                                                                     \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type &min(const type &other)                                                                                       \
    {                                                                                                                  \
        for (size_t i = 0; i < dimension; ++i)                                                                         \
        {                                                                                                              \
            this->elements[i] = std::min(this->elements[i], other.elements[i]);                                        \
        }                                                                                                              \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type &max(const type &other)                                                                                       \
    {                                                                                                                  \
        for (size_t i = 0; i < dimension; ++i)                                                                         \
        {                                                                                                              \
            this->elements[i] = std::max(this->elements[i], other.elements[i]);                                        \
        }                                                                                                              \
        return *this;                                                                                                  \
    }                                                                                                                  \
    T dot(const type &other) const                                                                                     \
    {                                                                                                                  \
        T sum = T(0);                                                                                                  \
        for (size_t i = 0; i < dimension; ++i)                                                                         \
        {                                                                                                              \
            sum += (*this)[i] * other.elements[i];                                                                     \
        }                                                                                                              \
        return sum;                                                                                                    \
    }                                                                                                                  \
    type &negate()                                                                                                     \
    {                                                                                                                  \
        TFuncArrayNegate(this->elements);                                                                              \
        return *this;                                                                                                  \
    }                                                                                                                  \
    T length() const                                                                                                   \
    {                                                                                                                  \
        T sum = T(0);                                                                                                  \
        for (size_t i = 0; i < dimension; ++i)                                                                         \
        {                                                                                                              \
            sum += elements[i] * elements[i];                                                                          \
        }                                                                                                              \
        return std::sqrt(sum);                                                                                         \
    }                                                                                                                  \
    T lengthSq()                                                                                                       \
    {                                                                                                                  \
        T sum = T(0);                                                                                                  \
        for (auto &i : elements)                                                                                       \
        {                                                                                                              \
            sum += i * i;                                                                                              \
        }                                                                                                              \
        return sum;                                                                                                    \
    }                                                                                                                  \
    T manhattanLength()                                                                                                \
    {                                                                                                                  \
        T sum = T(0);                                                                                                  \
        for (size_t i = 0; i < dimension; ++i)                                                                         \
        {                                                                                                              \
            sum += std::abs(elements[i]);                                                                              \
        }                                                                                                              \
        return sum;                                                                                                    \
    }                                                                                                                  \
    type &clamp(const type &min, const type &max)                                                                      \
    {                                                                                                                  \
        for (size_t i = 0; i < dimension; ++i)                                                                         \
        {                                                                                                              \
            elements[i] = std::max(min[i], std::min(max[i], elements[i]));                                             \
        }                                                                                                              \
        return *this;                                                                                                  \
    }                                                                                                                  \
    type &normalize()                                                                                                  \
    {                                                                                                                  \
        T length = this->length();                                                                                     \
        return this->divideScalar(length == T(0) ? T(1) : length);                                                     \
    }

// TVectorBase
template <class T, size_t dimension> struct POCELAIN_EXPORT TVector
{
    TVector()
    {
        TFuncArraySet(this->elements, T(0));
    }
    POCELAIN_DECLARE_VECTOR_COMMON_PART(dimension)
    template <class ArrayT> type &set(ArrayT arr)
    {
        size_t i = 0;
        for (auto item : arr)
        {
            elements[i] = item;
            ++i;
        }
        return *this;
    }
};

// TODO use inline
//  TVector2
template <class T> struct POCELAIN_EXPORT TVector<T, 2>
{
    POCELAIN_DECLARE_VECTOR_COMMON_PART(2)
    explicit TVector(const T x = T(0), const T y = T(0))
    {
        this->set(x, y);
    }
    type &set(const T x = T(0), const T y = T(0))
    {
        this->x = x;
        this->y = y;
        return *this;
    }
    T cross(const type &other)
    {
        return this->x * other.y - this->y * other.x;
    }
    T &x = elements[0];
    T &y = elements[1];

    T &u = elements[0];
    T &v = elements[1];
    type &applyMatrix3(const TMatrix<T, 3, 3> &m)
    {
        const auto x = this->x;
        const auto y = this->y;
        const auto &e = m.elements;
        this->x = e[0] * x + e[1] * y + e[2];
        this->y = e[3] * x + e[4] * y + e[5];
        return *this;
    }
};

// TVector3
template <class T> struct POCELAIN_EXPORT TVector<T, 3>
{
    POCELAIN_DECLARE_VECTOR_COMMON_PART(3)
    explicit TVector(const T x = T(0), const T y = T(0), const T z = T(0))
    {
        this->set(x, y, z);
    }
    T &x = elements[0];
    T &y = elements[1];
    T &z = elements[2];

    T &u = elements[0];
    T &v = elements[1];
    T &w = elements[2];
    type &set(const T x = T(0), const T y = T(0), const T z = T(0))
    {
        this->x = x;
        this->y = y;
        this->z = z;
        return *this;
    }
    type &cross(const type &other)
    {
        T ax = this->x, ay = this->y, az = this->z;
        T bx = other.x, by = other.y, bz = other.z;

        x = ay * bz - az * by;
        y = az * bx - ax * bz;
        z = ax * by - ay * bx;
        return *this;
    }
    type &crossVectors(const type &a, const type &b)
    {
        const auto ax = a.x, ay = a.y, az = a.z;
        const auto bx = b.x, by = b.y, bz = b.z;

        x = ay * bz - az * by;
        y = az * bx - ax * bz;
        z = ax * by - ay * bx;

        return *this;
    }

    type &applyMatrix3(const TMatrix<T, 3, 3> &m)
    {
        auto x = this->x, y = this->y, z = this->z;
        auto &e = m.elements;

        this->x = e[0] * x + e[1] * y + e[2] * z;
        this->y = e[3] * x + e[4] * y + e[5] * z;
        this->z = e[6] * x + e[7] * y + e[8] * z;

        return *this;
    }
    type &applyMatrix4(const TMatrix<T, 4, 4> &m)
    {
        auto x = this->x, y = this->y, z = this->z;
        auto &e = m.elements;

        auto w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15]);

        this->x = (e[0] * x + e[1] * y + e[2] * z + e[3]) * w;
        this->y = (e[4] * x + e[5] * y + e[6] * z + e[7]) * w;
        this->z = (e[8] * x + e[9] * y + e[10] * z + e[11]) * w;

        return *this;
    }
    type &applyNormalMatrix(const TMatrix<T, 3, 3> &m)
    {
        return this->applyMatrix3(m).normalize();
    }

    type &applyQuaternion(const TQuaternion<T> &q)
    {
        const auto x = this->x, y = this->y, z = this->z;
        const auto qx = q.x, qy = q.y, qz = q.z, qw = q.w;

        // calculate quat * vector

        const auto ix = qw * x + qy * z - qz * y;
        const auto iy = qw * y + qz * x - qx * z;
        const auto iz = qw * z + qx * y - qy * x;
        const auto iw = -qx * x - qy * y - qz * z;

        // calculate result * inverse quat

        this->x = ix * qw + iw * -qx + iy * -qz - iz * -qy;
        this->y = iy * qw + iw * -qy + iz * -qx - ix * -qz;
        this->z = iz * qw + iw * -qz + ix * -qy - iy * -qx;

        return *this;
    }
    type &transformDirection(const TMatrix<T, 4, 4> &m)
    {
        const auto x = this->x, y = this->y, z = this->z;
        const auto &e = m.elements;

        this->x = e[0] * x + e[1] * y + e[2] * z;
        this->y = e[4] * x + e[5] * y + e[6] * z;
        this->z = e[8] * x + e[9] * y + e[10] * z;

        return this->normalize();
    }
    type &setFromMatrixPosition(const TMatrix<T, 4, 4> &m)
    {
        const auto &e = m.elements;

        this->x = e[3];
        this->y = e[7];
        this->z = e[11];

        return *this;
    }

    type &setFromMatrixScale(const TMatrix<T, 4, 4> &m)
    {
        const auto sx = this->setFromMatrixColumn(m, 0).length();
        const auto sy = this->setFromMatrixColumn(m, 1).length();
        const auto sz = this->setFromMatrixColumn(m, 2).length();

        this->x(sx);
        this->y(sy);
        this->z(sz);

        return *this;
    }

    type &setFromMatrixColumn(const TMatrix<T, 4, 4> &m, int index)
    {
        this->x = m.elements[index + 0 * 4];
        this->y = m.elements[index + 1 * 4];
        this->z = m.elements[index + 2 * 4];
        return *this;
    }

    type &setFromMatrix3Column(const TMatrix<T, 3, 3> &m, int index)
    {
        this->x = m.elements[index + 0 * 3];
        this->y = m.elements[index + 1 * 3];
        this->z = m.elements[index + 2 * 3];
        return *this;
    }

    // TODO : no need to implement
    //    type &fromArray(const T *array, int offset = 0)
    //    {
    //        this->x = array[offset + 0];
    //        this->y = array[offset + 1];
    //        this->z = array[offset + 2];
    //        return *this;
    //    }

    T distanceToSquared(const type &v)
    {
        auto dx = this->x - v.x, dy = this->y - v.y, dz = this->z - v.z;
        return dx * dx + dy * dy + dz * dz;
    }
    type &setFromSpherical(const TSpherical<T> &s)
    {
        return this->setFromSphericalCoords(s.radius, s.phi, s.theta);
    }
    type &setFromSphericalCoords(T radius, T phi, T theta)
    {
        T sinPhiRadius = std::sin(phi) * radius;

        this->x = sinPhiRadius * std::sin(theta);
        this->y = std::cos(phi) * radius;
        this->z = sinPhiRadius * std::cos(theta);

        return *this;
    }
};

// TVector4
template <class T> struct POCELAIN_EXPORT TVector<T, 4>
{
    POCELAIN_DECLARE_VECTOR_COMMON_PART(4)
    explicit TVector(const T x = T(0), const T y = T(0), const T z = T(0), const T w = T(0))
    {
        this->set(x, y, z, w);
    }
    type &set(const T x = T(0), const T y = T(0), const T z = T(0), const T w = T(0))
    {
        this->x = x;
        this->y = y;
        this->z = z;
        this->w = w;
        return *this;
    }
    T &x = elements[0];
    T &y = elements[1];
    T &z = elements[2];
    T &w = elements[3];
};

#undef POCELAIN_DECLARE_VECTOR_COMMON_PART
POCELAIN_NAMESPACE_END
